#!/usr/bin/env python
# *-* encoding: utf8
# 
# Copyright (c) 2006 Stian Soiland
# 
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# 
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
#
# Author: Stian Soiland <stian@soiland.no>
# URL: http://soiland.no/i/src/
# License: LGPL
#
"""RDF- and store-based file system API.

"""

from rdflib import Namespace, BNode
FS = Namespace("http://perfs.googlecode.com/ontology/fs#")


import uuid
import datetime
from StringIO import StringIO
import rdfstore
from store import BlockStore

class Store(object):
    def __init__(self, store):
        self.store = store


block_store = BlockStore("/tmp/blockstore")

store = rdfstore.RDFStore(block_store)


class Filesystem(Store):
    def __init__(self, store, root):
        super(Filesystem, self).__init__(store)
        self.root = root

    def open(self, path, mode="r"):
        try:
            resource = self.root.get(path, 
                    create="r" not in mode)
        except KeyError:
            raise IOError(2, "No such file or directory: %r" % path)     
        f = resource.open(mode)
        return f
    
    def info(self, path):
        return self.root.get(path)     

    

class Resource(Store):
    def __init__(self, store, uuuid):
        super(Resource, self).__init__(store)
        self.uuid = uuid
        self.creationdate = datetime.datetime.now()
        self.lastmodified = self.creationdate

    def __eq__(self, other):
        if not hasattr(other, "uuid"):
            return False
        return self.uuid == other.uuid
    
    def __hash__(self):
        return hash(self.uuid)

    def _updated(self):          
        self.lastmodified = datetime.datetime.now()

    def paths(self):
        for directory, name in self.store.query("""
             PREFIX fs: <http://perfs.googlecode.com/ontology/fs#>
             SELECT ?directory ?name WHERE {
             ?resource fs:is_resource ?uri .
             ?resource fs:has_name ?name .
             ?resource fs:in_directory ?directory
             }
        """, uri=self.uuid.uri):
            yield directory.path() + "/" + name

    # Ignore the rest of the path by default
    def get(self, path, create=False):
        return self
   
    def __str__(self):
        return self.uuid.str
    
    def __repr__(self):
        return "<%s %s>" % (self.__class__.__name__, self.uuid.str)    

class DirectoryChildren(Store):
    """Dictionary-like container for children of a directory"""
    
    def __init__(self, store, directory):
        super(DirectoryChildren, self).__init__(store)
        self.directory = directory

    def __iter__(self):
        for name, uri in self.iteritems():
            yield name
  
    def iteritems(self):
        return self.store.query("""
             PREFIX fs: <http://perfs.googlecode.com/ontology/fs#>
             SELECT ?name ?uri WHERE {
             ?resource fs:is_resource ?uri .
             ?resource fs:has_name ?name .
             ?resource fs:in_directory ?directory
             }
        """, directory=self.directory.uri)
  
    def __setitem__(self, name, resource):
        try:
            del self[name]
        except KeyError:
            pass # OK
        # TODO: store new subgraph    
        entry = BNode()
        self.store.addN((
                (entry, FS.is_resource, resource.uri, self.store.graph),
                (entry, FS.has_name, name, self.store.graph),
                (entry, FS.in_directory, self.directory.uri, self.store.graph)))
        print "Added", name, resource

    
    def __delitem__(self, name):
        # Make a copy to avoid modifing in iteration
        for entry in self.store.query("""
             PREFIX fs: <http://perfs.googlecode.com/ontology/fs#>
             PREFIX rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
             SELECT ?entry WHERE {
             ?entry fs:has_name ?name .
             ?entry fs:in_directory ?directory .
             }
        """, name=name, directory=self.directory.uri):
            # We'll delete everything  with that entry bnode
            self.store.remove(entry, None, None)
            return
        # Didn't return? Name not there then..
        raise KeyError, name
    
    def __getitem__(self, name):
        for uri, type, entry, name, directory in self.store.query("""
             PREFIX fs: <http://perfs.googlecode.com/ontology/fs#>
             PREFIX rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
             SELECT ?uri ?type ?entry ?name ?directory WHERE {
             ?entry fs:in_directory ?directory .
             ?entry fs:is_resource ?uri .
             ?entry fs:has_name ?name .
             ?uri rdf:type ?type
             }
        """, name=name, directory=self.directory.uri):
            if type == FS.directory:
                return Directory(self.store, uri, 
                        parent=self.directory,
                        name=name)
            elif type == FS.file:
                return File(self.store, uri)
            else:
                raise "Unsupported type: " + type
        raise KeyError, name
    

class Directory(Resource):
    contenttype = "httpd/unix-directory"
    def __init__(self, store, uri, parent=None, name=None):
        super(Directory, self).__init__(store, uri) 
        self.parent = parent
        self.name = name
        self.children = DirectoryChildren(self.store, self)

    def root(self):
        if not self.parent:
            return self    
        return self.parent.root()    
    
    def add(self, name, resource):
        # FIXME: Make thread safe
        if name in self.children:
            # FIXME> Real exceptions
            raise AttributeError, "Already exist %s" % name
        self.children[name] = resource     
        self._updated()
    
    def remove(self, name):
        # FIXME: Make thread safe
        obj = self.children[name]
        if obj.parent == self and hasattr(obj, "parent"):
            obj.parent == None
        del self.children[name]   
        self._updated()

    def path(self):
        if not self.parent:
           return "/"
        return self.parent.path().rstrip("/") + "/" + self.name
                
    def get(self, path, create=False):
        if "/" in path:
            child, rest = path.split("/", 1)    
        else:
            child, rest = path, None
        if not child or child == ".":
            entry = self
        elif child == "..":
            entry = self.parent or self
        else:
            try:
                entry = self.children[child]      
            except KeyError, e:
                if not create or rest:
                    raise e
                # Make a new file
                uri = uuid.UUID().uri
                file = File(self.store, uri)
                self.children[child] = file
                return file

        if not rest:    
            return entry
        return entry.get(rest, create)    
    
    def open(self, mode=None):     
        raise IOError(21, "Is a directory")
            

class File(Resource):
    def __init__(self, store, uri):
        super(File, self).__init__(store, uri) 
        self.contenttype = "text/plain"
        self.data = ""

    def _get_data(self):
        return self._data 
    def _set_data(self, data):
        self._data = data
        self._updated()
    data = property(_get_data, _set_data)    
        
    def open(self, mode="r"):
        return FileObject(self, mode)
    
    @property
    def size(self):
        return len(self.data)

class FileObject(StringIO, object):
    def __init__(self, file, mode):
        super(FileObject, self).__init__(file.data)
        self.changed = False    
        # Before we have checked modes, nothing allowed
        self.readable = self.writable = False
        self.file = file
        self.mode = mode
        # Check mode 
        if "w" in mode:
            self.writable = True
            self.truncate()
        if "a" in mode:
            self.writable = True
        if "r" in mode:
            self.readable = True    
        if "+" in mode:
            self.writable = self.readable = True
        if "a" in mode:
            # Seek to the end of the file
            self.seek(0,2)    
   
    def _readable(method):
        """Decorator that wraps the given method to make sure
        the file is opened as readable."""
        def wrapper(self, *args, **kwargs):               
            if not self.readable:
                raise IOError(9, "Bad file descriptor")     
            return method(self, *args, **kwargs)    
        wrapper.func_name = method.func_name 
        wrapper.__doc__ = method.__doc__
        return wrapper

    def _writable(method=None, set_changed=True):
        """Decorator that wraps the given method to make sure
        the file is opened as writable. Optional parameter
        set_changed can be set to False to avoid updating
        self.changed.
        
        Thus this decorator can be used as either @_writable
        or @_writable(set_changed=False)
        """
        def decorator(method=method):
            def wrapper(self, *args, **kwargs):               
                if not self.writable:
                    raise IOError(9, "Bad file descriptor")     
                # Even if it might throw an exception, we 
                # don't know if it has changed or not
                # FIXME: flush() will set changed=True ! 
                if set_changed:
                    self.changed = True    
                return method(self, *args, **kwargs)    
            wrapper.func_name = method.func_name 
            wrapper.__doc__ = method.__doc__
            return wrapper
        # ARGHSS! To support both @_writable and _writable(False)    
        if method:
            return decorator()    
        else:    
            return decorator        

   
    # We'll just wrap these methods straight from our 
    # super class
    read = _readable(StringIO.read)
    readline = _readable(StringIO.readline)
    readlines = _readable(StringIO.readlines)
    __iter__ = _readable(StringIO.__iter__)
    next = _readable(StringIO.next)
                  
    truncate = _writable(StringIO.truncate)              
    write = _writable(StringIO.write)
    writelines = _writable(StringIO.writelines)
                  
    @_writable(set_changed=False)
    def flush(self):                
        """Flush written data back to original store"""
        if self.changed:
            self.file.data = self.getvalue()
                  
    def close(self):
        """Close the file object, flush if needed."""
        if self.writable:
            self.flush()
        super(FileObject, self).close()               
    
    def __del__(self):
        """Close the file on destruction. 
        Note that the GC behaviour is not deterministic, so don't rely
        on this to store your data when writing to the file."""
        try:
            self.close()    
        except:
            pass    
        

def dummies():
    store.remove(None, None, None)
    store.parse(open("../doc/dummy.n3"), format="n3")
    root = Directory(store, "urn:uuid:root")
    fs = Filesystem(store, root)
    return fs

        
